extends Camera

# Based on the trackball camera code at
#   https://github.com/Goutte/godot-trackball-camera
# Original license: MIT

class_name SimpleCamera

export(bool) var mouseEnabled: bool = true
export(bool) var mouseInvert: bool = false
export(float) var mouseStrength: float = 3.333
export(float) var zoomStrength: float = 1.111
export(bool) var keyboardEnabled: bool = true
export(bool) var keyboardInvert: bool = false
export(float) var keyboardStrength: float = 0.444
export(bool) var joystickEnabled: bool = true
export(bool) var joystickInvert: bool = false
export(float) var joystickStrength: float = 0.444
# The resting state of my joystick's x-axis is -0.05,
# so we want to ignore any input below this threshold.
export(float) var joystickThreshold: float = 0.09
export(int) var joystickDevice: int = 0
# Multiplier applied to all strengths
export(float) var inertiaStrength: float = 1.0
export(float, 0.01, 1, 0.001) var animationSpeed: float = 0.3

export(float, -90.0, 90.0, 0.1) var rot_x_max: float = 90.0
export(float, -90.0, 90.0, 0.1) var rot_x_min: float = -90.0

export(float) var distance_max: float = 1e+6
export(float) var distance_min: float = 0.0

var _distance: float
var _animated_distance: float

# [rotX, rotY]
var _rot: Vector2
var _animated_rot: Vector2

func recalculate_transform(is_animated: bool = false):
	# Solve rotation from position
	# FIXME: I use fixed axis
	var o: Vector3 = get_transform().origin
	var dist: float = o.length()
	var o2: Vector2 = Vector2(o.z, o.x)
	var dist2: float = o2.length()
	var rotY: float = 0.0 if dist2 < 1e-6 else atan2(o2.y, o2.x)
	var rotX: float = 0.0 if dist < 1e-6 else atan2(o.y, dist2)
	rotX = clamp(rotX, deg2rad(rot_x_min), deg2rad(rot_x_max))
	_rot = Vector2(rotX, rotY)
	_distance = clamp(dist, max(distance_min, near), min(distance_max, far))
	if not is_animated:
		_animated_rot = _rot
		_animated_distance = _distance
	# then apply transform
	apply_transform()

static func transform_from_rot(distance: float, rotX: float, rotY: float) -> Transform:
	return Transform.IDENTITY.translated(Vector3(0, 0, distance)) \
		.rotated(Vector3(1, 0, 0), -rotX) \
		.rotated(Vector3(0, 1, 0), rotY)

func apply_transform():
	set_transform(transform_from_rot(_animated_distance, _animated_rot.x, _animated_rot.y))

func _ready():
	recalculate_transform()

func _unhandled_input(ev):
	if mouseEnabled:
		if ev is InputEventMouseButton:
			if ev.pressed:
				match ev.button_index:
					BUTTON_WHEEL_UP:
						if mouseInvert:
							_distance *= zoomStrength
						else:
							_distance /= zoomStrength
					BUTTON_WHEEL_DOWN:
						if mouseInvert:
							_distance /= zoomStrength
						else:
							_distance *= zoomStrength
		elif ev is InputEventMouseMotion:
			if ev.button_mask == BUTTON_LEFT:
				var r: Vector2 = get_viewport().get_visible_rect().size
				var r_size: float = max(r.x, r.y) + 1.0
				var inertia: Vector2 = ev.relative / r_size \
					* mouseStrength * (-1 if mouseInvert else 1)
				add_inertia(inertia * inertiaStrength)

func _process(delta):
	var inertia: Vector2 = Vector2(0, 0)

	if keyboardEnabled:
		var key_i = 1 if keyboardInvert else -1
		var key_s = keyboardStrength * delta
		if Input.is_key_pressed(KEY_LEFT):
			inertia += Vector2(key_i * key_s, 0)
		if Input.is_key_pressed(KEY_RIGHT):
			inertia += Vector2(-1 * key_i * key_s, 0)
		if Input.is_key_pressed(KEY_UP):
			inertia += Vector2(0, key_i * key_s)
		if Input.is_key_pressed(KEY_DOWN):
			inertia += Vector2(0, -1 * key_i * key_s)

	if joystickEnabled:
		var joy_h = Input.get_joy_axis(joystickDevice, 0) # left stick horizontal
		var joy_v = Input.get_joy_axis(joystickDevice, 1) # left stick vertical
		var joy_i = 1 if joystickInvert else -1
		var joy_s = joystickStrength * delta

		if abs(joy_h) > joystickThreshold:
			inertia += Vector2(joy_i * joy_h * joy_h * sign(joy_h) * joy_s, 0)
		if abs(joy_v) > joystickThreshold:
			inertia += Vector2(0, joy_i * joy_v * joy_v * sign(joy_v) * joy_s)

	add_inertia(inertia * inertiaStrength)

	var r_min: float = deg2rad(rot_x_min)
	var r_max: float = deg2rad(rot_x_max)
	var d_min: float = max(distance_min, near)
	var d_max: float = min(distance_max, far)

	if (_animated_rot - _rot).length() > 1e-4 or abs(_animated_distance - _distance) > 1e-4:
		if _rot.x < r_min:
			_rot.x += (r_min - _rot.x) * animationSpeed
		elif _rot.x > r_max:
			_rot.x += (r_max - _rot.x) * animationSpeed
		if _distance < d_min:
			_distance += (d_min - _distance) * animationSpeed
		elif _distance > d_max:
			_distance += (d_max - _distance) * animationSpeed
		_animated_rot += (_rot - _animated_rot) * animationSpeed
		_animated_distance += (_distance - _animated_distance) * animationSpeed
	else:
		if _rot.y > 2 * PI:
			_rot.y -= 2 * PI
		if _rot.y < -2 * PI:
			_rot.y += 2 * PI
		_rot.x = clamp(_rot.x, r_min, r_max)
		_distance = clamp(_distance, d_min, d_max)
		_animated_rot = _rot
		_animated_distance = _distance

	apply_transform()


# Convenience method for you to move the camera around.
# inertia is a Vector2 in the normalized right-handed x/y of the screen.
func add_inertia(inertia):
	_rot.x += inertia.y
	_rot.y -= inertia.x
